查看原文
其他

如何创建多进程程序?(文末福利)

守望先生 编程珠玑 2021-01-31


来源:公众号【编程珠玑】

作者:守望先生

网站:https://www.yanbinghu.com

前言

在《对进程和线程的一些总结》已经介绍了进程和线程的区别,但是在C/C++中如何创建进程呢?或者说如何编写多进程的程序呢?

什么时候需要fork进程

一种可能见到的场景是在服务器程序中,一个请求到来后,为了避免服务器阻塞,fork出一个子进程处理请求,父进程仍然继续等待请求到来。但这种方式无疑开销会稍大。

另一种最常见的就是执行一个不同的程序,例如我们在shell终端执行一条命令,实际上就是bash(或者其他)调用fork之后,在执行exec族函数。

fork

一个现有的进程可以通过fork函数来创建一个新的进程,这个进程通常称为子进程。fork函数原型如下:

#include<unistd.h>
pid_t fork(void);

如果调用成功,它将返回两次,子进程返回值是0;父进程返回的是非0正值,表示子进程的进程id;如果调用失败将返回-1,并且置errno变量。

有的朋友可能常常会记不住返回0的时候到底是子进程还是父进程。这里教给大家一个方法。一个进程可以有多个子进程,但是一个子进程同一时刻最多只有一个父进程。子进程可以通过getppid获取父进程的进程id,但是父进程却没法获取,因此需要在fork后就得到子进程的进程id。

//公众号【编程珠玑】,博客 https://www.yanbinghu.com
#include<stdio.h>
#include<unistd.h>
int main(void)
{
    pid_t pid;
    char testVal[128] = {0};
    FILE *fp = fopen("test.txt","w");
    if(NULL == fp)
    {
        printf("open test.txt failed\n");
        return 0;
    }
    if(-1 == (pid = fork()))//等于-1时表明fork出错
    {
        perror("fork error");
        return -1;
    }
    else if(0 == pid)//子进程
    {
        snprintf(testVal,128,"I am child,father pid is %d\n",getppid());
        fprintf(fp,"%s",testVal);

    }
    else  //父进程
    {
        snprintf(testVal,128,"I am parent,child pid is %d\n",pid);
        fprintf(fp,"%s",testVal);
    }
    printf("fork over,testVal is %s",testVal);
    //为了避免马上退出sleep一段时间
    sleep(100);
    fclose(fp);
    return 0;
}

并在同目录下创建一个test.txt文件,运行结果:

fork over,testVal is I am parent,child pid is 13008
fork over,testVal is I am child,father pid is 13007

需要注意的是,不要对父进程先执行还是子进程先执行做任何假设,因为都有可能。所以,可能出现的运行结果并不一样。

fork到底做了什么

fork被调用后,子进程拥有父进程的副本,因此它拥有父进程的数据空间,堆栈等。但是由于fork之后通常会调用exec函数去执行与原进程不想关的程序,因此fork时直接拷贝父进程的副本显得没有必要。为了提高fork的效率,采用了一种写时复制的技术。即fork之后,子进程名义上拥有父进程的副本,但是实际上和父进程共用,只有当父子进程中有一个试图修改这些区域时,才会以页为单位创建一个真正的副本。

所以我们看到前面的示例程序中,父子进程都对testVal进程了修改,但是互不影响。因为它们修改了不同的区域。

子进程继承了父进程哪些属性?

由于子进程是父进程的一个副本,所以父进程有的属性,子进程也都有,这些属性包括

  • 打开的文件描述符

  • 会话ID

  • 根目录

  • 资源限制

  • 工作目录

  • 进程组ID

  • 控制终端

  • 环境

我们运行前面的示例程序之后,重新打开一个终端,找到打开test.txt文件的进程:

$ lsof test.txt
fork    9919 root    3r   REG  252,1        0 396427 test.txt
fork    9920 root    3r   REG  252,1        0 396427 test.txt

lsof命令的用法可以参考《如何查看linux中文件打开情况?

也可以观察进程打开的文件描述符:

$ ls -l /proc/9919/fd
lrwx------ 1 root root 64 Aug 10 15:38 0 -> /dev/pts/1
lrwx------ 1 root root 64 Aug 10 15:38 1 -> /dev/pts/1
lrwx------ 1 root root 64 Aug 10 15:38 2 -> /dev/pts/1
lr-x------ 1 root root 64 Aug 10 15:38 3 -> /data/workspaces/practices/c/test.txt

为什么这里要特别说明打开的文件描述符呢?试想以下两点:

  • 父子进程对同一个文件进行写,将共享文件偏移

  • 如果该描述符是一个socket描述符,父进程退出后,子进程仍然打开着,父进程再次启动,将会出现端口被占用的问题。

所以如果父子进程的其中一个使用了fclose关闭了文件描述符,实际上还有另外一个进程打开了test.txt文件。

与前面testVal不同的是,如果父子进程都对文件进行写,并不会产生两个不同的文件,而是会对同一个文件进行写,因此运行后会在同一个文件里出现父子进程写的内容:

$ cat test.txt
I am parent,child pid is 13008
I am child,father pid is 13007

父子进程有哪些不同?

  • fork之后的返回值不同,进程ID也不同

  • 子进程未处理信号设置为空

  • 子进程不继承父进程设置的文件锁

  • 一般子进程会执行与父进程不完全一样的代码流程

总结

fork用于创建进程,但是需要注意的是,子进程继承了很多父进程的东西,如果子进程不需要可以进行修改或“丢弃”,例如子进程关闭父进程打开的文件描述符等等。理解了fork的写时复制思想,也就会明白,实际上fork的速度是非常快的。本文总结点如下:

  • fork调用一次,返回两次

  • 一个进程可以有多个子进程,但同一时刻最多只有一个父进程

  • 子进程继承了父进程很多属性

  • 父子进程执行的先后顺序不一定

本文仅仅简单介绍了fork,实际上得到子进程之后,还需要对子进程的状态进行“监控”,否则会出现其他意想不到的问题。

福利来啦

《边学边玩Scratch3.0少儿编程》是美国权威Scratch培训机构教材原版引进,与美国孩子同步学习最新版Scratch课程,麻省理工学院终身幼儿园团队Scratch软件开发项目组官方授权,6-14岁零基础孩子的第一本轻松上手Scratch少儿编程书,启蒙从这本书开始!


在此文留言评论,点赞数最高者三位将获得本书一本时间截止8月17日上午10点。当然如果你需要的话也可以点击阅读原文进行购买。



关注公众号【编程珠玑】,第一时间获取更多原创技术文章

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存